--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 538c4d72cfdeebf10e40333522463f615562d783
Parents : 5eb02b2
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-03T13:18:57-05:00
feat(meshchat): update conversation previews with user display names and improve telemetry handling
Changes
4 files changed, 230 insertions(+), 21 deletions(-)
Diff
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 4b875a2a..734cb199 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -10971,6 +10971,10 @@ class ReticulumMeshChat:
icon = self.database.misc.get_user_icon(other_user_hash)
+ peer_preview_name = (
+ custom_display_name or display_name or "Anonymous Peer"
+ )
+
conversations.append(
{
"type": "lxmf_message",
@@ -10979,8 +10983,12 @@ class ReticulumMeshChat:
"custom_display_name": custom_display_name,
"lxmf_user_icon": dict(icon) if icon else None,
"latest_message_preview": (
- latest_message_data["content"] or ""
- )[:100],
+ lxmf_sidebar_preview_for_conversation_latest_row(
+ dict(latest_for_preview),
+ local_hash=local_hash,
+ peer_display_name=peer_preview_name,
+ )[:100]
+ ),
"updated_at": datetime.fromtimestamp(
latest_message_data["timestamp"] or 0,
UTC,
diff --git a/meshchatx/src/backend/lxmf_utils.py b/meshchatx/src/backend/lxmf_utils.py
index 6c2dac93..34eeb48c 100644
--- a/meshchatx/src/backend/lxmf_utils.py
+++ b/meshchatx/src/backend/lxmf_utils.py
@@ -28,10 +28,15 @@ def is_user_facing_lxmf_payload(fields, content, title) -> bool:
Messages that should NOT raise the notification bell:
- reactions (Columba app_extensions.reaction_to with no other payload)
- - telemetry-only messages (FIELD_TELEMETRY / fields["telemetry"] with no body)
+ - bare telemetry updates with no coordinates, no stream, and no
+ Sideband location-request command (FIELD_TELEMETRY body-only noise)
- icon-only / appearance-only updates (no body, no attachment)
- empty pings (no content, no title, no attachment)
+ Location shares (telemetry including ``location``), telemetry streams,
+ and Sideband ``commands`` entries with key ``0x01`` (location request) ARE
+ treated as user-facing so the bell and previews stay informative.
+
The helper is intentionally tolerant: ``fields`` may be the rich dict
produced by :func:`convert_lxmf_message_to_dict` (string keys), the raw
LXMF integer-keyed dict, or a JSON-string from the database.
@@ -92,6 +97,22 @@ def is_user_facing_lxmf_payload(fields, content, title) -> bool:
if isinstance(raw_files, list) and len(raw_files) > 0:
return True
+ telemetry = fields.get("telemetry")
+ if isinstance(telemetry, dict):
+ loc = telemetry.get("location")
+ if isinstance(loc, dict) and loc:
+ return True
+
+ ts = fields.get("telemetry_stream")
+ if isinstance(ts, list) and len(ts) > 0:
+ return True
+
+ commands = fields.get("commands")
+ if isinstance(commands, list):
+ for cmd in commands:
+ if isinstance(cmd, dict) and "0x01" in cmd:
+ return True
+
return False
@@ -109,6 +130,22 @@ def _reaction_emoji_from_parsed_lxmf_fields(fields: dict) -> str | None:
return None
+def _lxmf_sidebar_actor_label(
+ row: dict,
+ *,
+ local_hash: str,
+ peer_display_name: str,
+) -> str:
+ is_incoming = bool(row.get("is_incoming"))
+ if is_incoming:
+ return peer_display_name or "Anonymous Peer"
+ src = (row.get("source_hash") or "").lower()
+ loc = (local_hash or "").lower()
+ return (
+ "You" if src and loc and src == loc else (peer_display_name or "Anonymous Peer")
+ )
+
+
def lxmf_sidebar_preview_for_conversation_latest_row(
row: dict,
*,
@@ -131,23 +168,39 @@ def lxmf_sidebar_preview_for_conversation_latest_row(
except (json.JSONDecodeError, TypeError):
fields = {}
- emoji = _reaction_emoji_from_parsed_lxmf_fields(fields)
- if not emoji:
- return str(content or "")
+ actor = _lxmf_sidebar_actor_label(
+ row,
+ local_hash=local_hash,
+ peer_display_name=peer_display_name,
+ )
+ incoming = bool(row.get("is_incoming"))
- is_incoming = bool(row.get("is_incoming"))
- if is_incoming:
- actor = peer_display_name or "Anonymous Peer"
- else:
- src = (row.get("source_hash") or "").lower()
- loc = (local_hash or "").lower()
- actor = (
- "You"
- if src and loc and src == loc
- else (peer_display_name or "Anonymous Peer")
- )
-
- return f"{actor} reacted {emoji}"
+ emoji = _reaction_emoji_from_parsed_lxmf_fields(fields)
+ if emoji:
+ return f"{actor} reacted {emoji}"
+
+ telemetry = fields.get("telemetry")
+ if isinstance(telemetry, dict):
+ loc = telemetry.get("location")
+ if isinstance(loc, dict) and loc:
+ return f"{actor} shared their location"
+
+ ts = fields.get("telemetry_stream")
+ if isinstance(ts, list) and len(ts) > 0:
+ return f"{actor} sent a telemetry stream"
+
+ if isinstance(telemetry, dict) and len(telemetry) > 0:
+ return f"{actor} sent telemetry"
+
+ commands = fields.get("commands")
+ if isinstance(commands, list):
+ for cmd in commands:
+ if isinstance(cmd, dict) and "0x01" in cmd:
+ if incoming:
+ return f"{actor} requested your location"
+ return f"{actor} sent a location request"
+
+ return str(content or "")
def convert_lxmf_message_to_dict(
diff --git a/tests/backend/test_lxmf_utils_extended.py b/tests/backend/test_lxmf_utils_extended.py
index 43aca007..850289d1 100644
--- a/tests/backend/test_lxmf_utils_extended.py
+++ b/tests/backend/test_lxmf_utils_extended.py
@@ -334,3 +334,67 @@ def test_sidebar_preview_prefers_non_empty_content():
peer_display_name="Eve",
)
assert out == " hi "
+
+
+def test_sidebar_preview_telemetry_location_incoming():
+ local = "a" * 32
+ row = {
+ "content": "",
+ "fields": json.dumps(
+ {"telemetry": {"location": {"latitude": 1.0, "longitude": 2.0}}},
+ ),
+ "is_incoming": 1,
+ "source_hash": "b" * 32,
+ }
+ out = lxmf_sidebar_preview_for_conversation_latest_row(
+ row,
+ local_hash=local,
+ peer_display_name="Riley",
+ )
+ assert out == "Riley shared their location"
+
+
+def test_sidebar_preview_location_request_outbound_you():
+ me = "c" * 32
+ row = {
+ "content": "",
+ "fields": json.dumps({"commands": [{"0x01": 1_700_000_000}]}),
+ "is_incoming": 0,
+ "source_hash": me,
+ }
+ out = lxmf_sidebar_preview_for_conversation_latest_row(
+ row,
+ local_hash=me,
+ peer_display_name="Sam",
+ )
+ assert out == "You sent a location request"
+
+
+def test_sidebar_preview_telemetry_battery_only():
+ row = {
+ "content": "",
+ "fields": json.dumps({"telemetry": {"battery": {"charge_percent": 50}}}),
+ "is_incoming": 1,
+ "source_hash": "b" * 32,
+ }
+ out = lxmf_sidebar_preview_for_conversation_latest_row(
+ row,
+ local_hash="a" * 32,
+ peer_display_name="Taylor",
+ )
+ assert out == "Taylor sent telemetry"
+
+
+def test_sidebar_preview_telemetry_stream():
+ row = {
+ "content": "",
+ "fields": json.dumps({"telemetry_stream": [{"t": 1}]}),
+ "is_incoming": 1,
+ "source_hash": "b" * 32,
+ }
+ out = lxmf_sidebar_preview_for_conversation_latest_row(
+ row,
+ local_hash="a" * 32,
+ peer_display_name="Jordan",
+ )
+ assert out == "Jordan sent a telemetry stream"
diff --git a/tests/backend/test_notification_user_facing_filter.py b/tests/backend/test_notification_user_facing_filter.py
index c8addd60..714a6a1d 100644
--- a/tests/backend/test_notification_user_facing_filter.py
+++ b/tests/backend/test_notification_user_facing_filter.py
@@ -10,8 +10,10 @@ Covers:
- the DAO method
:func:`MessageDAO.get_latest_user_facing_incoming_message`
- end-to-end ``GET /api/v1/notifications`` integration: reactions,
- telemetry-only, icon-only, empty pings and delivery-status updates
- must not produce false unread badges or empty dropdown entries
+ generic telemetry-only payloads, icon-only, empty pings and
+ delivery-status updates must not produce false unread badges or empty
+ dropdown entries; location shares, telemetry streams, and Sideband
+ location requests must surface with a readable preview.
"""
from __future__ import annotations
@@ -75,6 +77,18 @@ class TestIsUserFacingLxmfPayload:
fields = {"telemetry": {"some": "data"}}
assert not is_user_facing_lxmf_payload(fields, "", "")
+ def test_telemetry_with_location_is_user_facing(self):
+ fields = {"telemetry": {"location": {"latitude": 1.0, "longitude": 2.0}}}
+ assert is_user_facing_lxmf_payload(fields, "", "")
+
+ def test_telemetry_stream_is_user_facing(self):
+ fields = {"telemetry_stream": [{"x": 1}]}
+ assert is_user_facing_lxmf_payload(fields, "", "")
+
+ def test_sideband_location_request_command_is_user_facing(self):
+ fields = {"commands": [{"0x01": 1_700_000_000}]}
+ assert is_user_facing_lxmf_payload(fields, "", "")
+
def test_icon_only_is_not_user_facing(self):
# Icon appearance updates are processed separately and never appear in
# the converted ``fields`` dict; an icon-only message therefore looks
@@ -167,6 +181,19 @@ class TestRequireUserFacingFlag:
is False
)
+ def test_telemetry_location_unread_when_require_user_facing(self):
+ row = _row(
+ incoming=1,
+ fields={"telemetry": {"location": {"latitude": 0.0, "longitude": 0.0}}},
+ )
+ assert (
+ compute_lxmf_conversation_unread_from_latest_row(
+ row,
+ require_user_facing=True,
+ )
+ is True
+ )
+
def test_user_facing_message_still_unread(self):
row = _row(incoming=1, content="hello")
assert (
@@ -305,6 +332,24 @@ class TestGetLatestUserFacingIncomingMessage:
result = db.messages.get_latest_user_facing_incoming_message(PEER_HASH)
assert result is None
+ def test_returns_incoming_location_telemetry(self, db):
+ db.messages.upsert_lxmf_message(
+ _mk_message(
+ msg_hash="loc1",
+ peer_hash=PEER_HASH,
+ content="",
+ fields={
+ "telemetry": {
+ "location": {"latitude": 1.0, "longitude": 2.0},
+ },
+ },
+ timestamp=200,
+ ),
+ )
+ result = db.messages.get_latest_user_facing_incoming_message(PEER_HASH)
+ assert result is not None
+ assert result["hash"] == "loc1"
+
def test_skips_outgoing_messages(self, db):
db.messages.upsert_lxmf_message(
_mk_message(
@@ -527,6 +572,45 @@ class TestNotificationsGetUserFacingFilter:
assert body["unread_count"] == 0
assert body["notifications"] == []
+ async def test_telemetry_location_raises_bell_with_preview(self, bell_app):
+ bell_app.database.messages.upsert_lxmf_message(
+ _mk_message(
+ msg_hash="loc1",
+ peer_hash=PEER_HASH,
+ content="",
+ fields={
+ "telemetry": {
+ "location": {"latitude": 1.0, "longitude": 2.0},
+ },
+ },
+ timestamp=1_700_000_000,
+ ),
+ )
+ body = await self._get(bell_app, unread="true", limit=10)
+ assert body["unread_count"] == 1
+ assert len(body["notifications"]) == 1
+ peer_label = f"peer-{PEER_HASH[:6]}"
+ assert body["notifications"][0]["latest_message_preview"] == (
+ f"{peer_label} shared their location"
+ )
+
+ async def test_sideband_location_request_raises_bell_with_preview(self, bell_app):
+ bell_app.database.messages.upsert_lxmf_message(
+ _mk_message(
+ msg_hash="req1",
+ peer_hash=PEER_HASH,
+ content="",
+ fields={"commands": [{"0x01": 1_700_000_000}]},
+ timestamp=1_700_000_000,
+ ),
+ )
+ body = await self._get(bell_app, unread="true", limit=10)
+ assert body["unread_count"] == 1
+ peer_label = f"peer-{PEER_HASH[:6]}"
+ assert body["notifications"][0]["latest_message_preview"] == (
+ f"{peer_label} requested your location"
+ )
+
async def test_empty_payload_does_not_raise_bell(self, bell_app):
bell_app.database.messages.upsert_lxmf_message(
_mk_message(
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────